Skip to content

fix(pkg): vendored 头输给宿主机 —— compat.ffmpeg / compat.catch2 - #183

Merged
Sunrisepeak merged 1 commit into
mainfrom
fix/vendored-headers-lose-to-host
Aug 8, 2026
Merged

fix(pkg): vendored 头输给宿主机 —— compat.ffmpeg / compat.catch2#183
Sunrisepeak merged 1 commit into
mainfrom
fix/vendored-headers-lose-to-host

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

两个 compat 包在装了同名系统开发包的机器上会拿到宿主机的头而不是 vendored 的那份。CI 和本地都看不见,因为默认工具链 gcc 通过 --sysroot 进 xlings subos 编译,那里 /usr/include 干干净净;换到 llvm(无 sysroot)当场就炸。

复现:一台装了 libavutil-dev 6.1.1 的 Ubuntu,用 llvm 构建任何 compat.ffmpeg 消费者。

compat.ffmpeg

源码根走 include_dirs_after 是 #249 为 macOS 修的(大小写不敏感文件系统上 ffmpeg 的 VERSION 会遮蔽 libc++ 的 <version>)。但 -idirafter 排在系统目录之后,于是 vendored 8.1.2 的每一个 #include "libavutil/..." 都输给宿主机的 6.1.1。

不是优雅降级 —— 全局 -DHAVE_AV_CONFIG_H 让宿主机的公开头去 include dev 包根本不装的私有头:

/usr/include/x86_64-linux-gnu/libavutil/bswap.h:48:13:
  fatal error: 'x86/bswap.h' file not found

外加 6.x/8.x 类型错配(enum AVAlphaMode 不完整、SwsContext 少 typedef)。

改成按 OS 放置,且只能二选一:同一目录同时出现在 -I-idirafter 会被编译器去重到最后那个位置,留一份冗余的 -idirafter 会静默抵消 -I(clang 22.1.8 实测:同一条命令行只删掉重复的 -idirafter 就编过 —— 这也是第一版"加上去而不拿掉"的修法完全无效的原因)。

OS 放哪 为什么
linux include_dirs 文件系统大小写敏感,VERSION 遮蔽不了 <version>;且是唯一有系统 ffmpeg 可输的平台
macosx include_dirs_after #249 原样
windows include_dirs_after NTFS 同样大小写不敏感,换 -I 会把 #249 带回来;而那边系统目录里没有 libav*

生成器 tools/compat-ffmpeg/gen_multiplatform.py 同步改。per-OS 快照已不在,描述符是手改的,两边保持一致。

compat.catch2

上游 2.13.10 的单头调了两次 new(std::nothrow)(catch.hpp:977 / :14594)却从不 #include <new>。libstdc++ 会从别的头把它带进来,libc++ 不会 —— 所以每个 clang 消费者都挂在 no member named 'nothrow' in namespace 'std'。Catch2 v2 上游已 EOL。

修法是 mcpp_generated 里放 shim(#include <new>#include_next 到上游那份),而不是 patch 一份 17k 行的头 —— sha256 钉住的 tarball 仍是唯一事实来源。这要求 mcpp_generated 排到 include_dirs 最前,因为 #include_next 从本文件所在目录之后继续搜。

不能用 cflags 解:描述符的 cflags 到不了消费者的 TU(实测消费端只拿到 include_dirs-std),而 catch.hpp 恰恰是消费者包含的。

删除冻结的 mcpplibs:opencv@0.0.10

#165 已迁到 opencv:opencv@5.0.0 并冻结保留。索引内三个消费成员早就是限定形式,删除零 fallout;顺带修掉三处说自己在消费裸名的过时注释。

⚠️ mcpp new -t opencv:imgproc 会随之失效 —— -t 的 spec 语法里冒号是模板分隔符,表达不出 ns:name,所以在 mcpp 给限定名留出位置之前,模板路径拿不到 opencv。

验证

一台装了 ffmpeg 6.1.1 dev 头和 Catch2 v3 的机器,llvm@22.1.8 与 gcc@16.1.0 双工具链:

结果
ffmpeg 消费者 @ llvm 2311.o(libswscale 71,原先整批全灭);二进制断言 avutil_version()>>16 == 60 通过 —— 用的确是 vendored 8.1.2
gcc 回归 2281 个 .o,通过,走新的 -I<root> + sysroot
opencv 端到端 @ llvm import opencv.cv; + cvtColor/GaussianBlur 通过
ffmpeg 成员 @ llvm ok(83s)—— 这是描述符的专属验证成员
catch2 / catch2-main / catch2-v2 双工具链全过
80 个描述符 mcpp xpkg parse + 完整 lint 全过

已知未修(均为既有问题,已用控制变量验证)

1. ffmpeg-module / opencv-module 在 llvm 下仍失败。 这两个成员的 [indices] 声明的是各自的命名空间,按"成员级 [indices] 替换而非合并"的语义顶掉了根部的 compat 重定向,于是它们的 compat.ffmpeg已发布索引解析 —— 那份还是旧的。这正是 opencv-module 自己注释里写的设计(「The compat descriptor itself is pre-merge-validated by its own dedicated member」),而那个专属成员 ffmpeg 在本分支上是绿的。本 PR 合并 + 索引重新发布后自动消失。 回退本 PR 后这两个成员报一模一样的错。

2. catch2-v2-main 在 llvm 下失败。 catch2_main.cpp__has_include(<catch2/catch_all.hpp>) 判 v2/v3,该探测同样会落到系统目录,在装了系统 Catch2 v3 的机器上把 v2 判成 v3,链接报 undefined Catch::Session。同一类问题,但要不问文件系统就知道版本,得等 per-version build blocks(mcpp#290)。回退本 PR 后同样失败。

后续

有一份配套改动(CI 的 linux 加 llvm 工具链维度 + 在 llvm 腿上装 libav*-dev)没有放进本 PR,因为它叠在 fix/graphics-relanding-with-side-effect-test 尚未合并的 CI 工作之上。

顺序必须是:本 PR 合并 → 索引重新发布 → 再落 CI 的 llvm 腿。反过来的话那条新腿一上来就是红的(ffmpeg / catch2 的修复都在这里,而且 module 类成员还要等发布)。

两个包在装了同名系统开发包的机器上会拿错头,而 CI 和本地一直看不见:默认
工具链 gcc 是通过 --sysroot 进 xlings subos 编译的,那里 /usr/include 干干
净净。换到 llvm(无 sysroot)宿主机的头就进了默认搜索路径,两个都当场炸。

## compat.ffmpeg:-idirafter 排在系统目录之后

源码根走 include_dirs_after 是 #249 为 macOS 修的 —— 大小写不敏感的文件系统
上 ffmpeg 的 VERSION 会遮蔽 libc++ 的 <version>。但 -idirafter 的语义是排在
**系统目录之后**,于是在装了 libavutil-dev 的 Debian/Ubuntu 上
(/usr/include/x86_64-linux-gnu 是默认搜索目录),vendored 8.1.2 的每一个
`#include "libavutil/..."` 都输给宿主机的 6.1.1。

不是优雅降级:全局 -DHAVE_AV_CONFIG_H 让宿主机的**公开**头去 include dev 包
根本不装的私有头('x86/bswap.h'、libavutil/internal.h),再叠加 6.x/8.x 的
类型错配(enum AVAlphaMode 不完整、SwsContext 少 typedef)。

改成按 OS 放置,且**只能二选一**:同一个目录同时出现在 -I 和 -idirafter 会
被编译器去重到最后那个位置,保留一份冗余的 -idirafter 就会静默抵消 -I。
clang 22.1.8 上实测过:同一条命令行,只删掉重复的 -idirafter 就编过。

  linux   → include_dirs。文件系统大小写敏感,VERSION 遮蔽不了 <version>;
            而它是唯一有系统 ffmpeg 可输的平台。
  macosx  → include_dirs_after,#249 原样。
  windows → include_dirs_after。NTFS 同样大小写不敏感,换 -I 会把 #249 带
            回来,而那边的系统目录里没有 libav*。

生成器 tools/compat-ffmpeg/gen_multiplatform.py 同步改。per-OS 快照已不在,
描述符是手改的,两边保持一致。

## compat.catch2:上游 2.13.10 漏 #include <new>

单头调了两次 new(std::nothrow)(catch.hpp:977 / :14594)却从不包含 <new>。
libstdc++ 会从别的头把它带进来,libc++ 不会,于是每个 clang 消费者都挂在
"no member named 'nothrow' in namespace 'std'"。Catch2 v2 上游已 EOL。

修法是在 mcpp_generated 里放 shim:#include <new> 后 #include_next 到上游那
份,而不是 patch 一份 17k 行的头 —— sha256 钉住的 tarball 仍是唯一事实来源。
这要求 mcpp_generated 排到 include_dirs 最前,因为 #include_next 从本文件所在
目录之后继续搜。

不能用 cflags 解:描述符的 cflags 到不了消费者的 TU(实测消费端只拿到
include_dirs 和 -std),而 catch.hpp 恰恰是消费者包含的。

## 删除冻结的 mcpplibs:opencv@0.0.10

#165 已把它迁到 opencv:opencv@5.0.0 并冻结保留。索引内三个消费成员早就写成
限定形式,删除零 fallout;顺带修掉三处说自己在消费裸名的过时注释。

注意 `mcpp new -t opencv:imgproc` 会随之失效 —— `-t` 的 spec 语法里冒号是
模板分隔符,表达不出 ns:name,所以在 mcpp 给限定名留出位置之前,模板路径
拿不到 opencv。

## 验证

在一台装了 ffmpeg 6.1.1 dev 头和 Catch2 v3 的机器上,llvm@22.1.8 与
gcc@16.1.0 双工具链:

  ffmpeg 消费者   llvm 2311 个 .o(libswscale 71,原先整批全灭);二进制断言
                  avutil_version()>>16 == 60 通过 —— 用的确是 vendored 8.1.2
  gcc 回归        2281 个 .o,同样通过,走新的 -I<root> + sysroot
  opencv 端到端   import opencv.cv; + cvtColor/GaussianBlur,llvm 下通过
  catch2          catch2 / catch2-main / catch2-v2 三成员双工具链通过

已知未修:catch2-v2-main 在 llvm 下仍失败。catch2_main.cpp 用
__has_include(<catch2/catch_all.hpp>) 判 v2/v3,该探测同样会落到系统目录,
在装了系统 Catch2 v3 的机器上把 v2 判成 v3,链接报 undefined Catch::Session。
同一类问题,但要不问文件系统就知道版本,得等 per-version build blocks
(mcpp#290)。回退本提交后同样失败,是既有问题而非本次引入。
@Sunrisepeak
Sunrisepeak merged commit 4376903 into main Aug 8, 2026
9 checks passed
Sunrisepeak added a commit that referenced this pull request Aug 8, 2026
#183 修的两个包(compat.ffmpeg 的 -idirafter 排在系统目录之后、compat.catch2
漏 #include <new>)在这份 CI 里一直是绿的,不是因为它们对,而是因为这份 CI
结构上看不见它们:

  * mcpp 在 linux 的默认工具链是 gcc,而它通过 --sysroot 进 xlings subos 编
    译 —— 那里的 /usr/include 干干净净,宿主机的头根本不在搜索路径上。
  * 就算换了工具链,GitHub runner 的 /usr/include 里也没有 libav*,-idirafter
    没有东西可输。

所以这条腿要同时改两件事才有意义:换 llvm(无 sysroot),并真的把 ffmpeg 的
dev 头装上。只做前者,本次这个 bug 照样照不出来。

## 矩阵

emit() 多一个 toolchain 维度,linux 发两次(default + llvm),macos / windows
保持 default。job 名只在非 default 时才带工具链后缀,所以既有 job 名不变。

成本是实打实的:linux 从 3 个 shard 变 6 个,而实测 linux runner 并发是 3,
所以第二条腿是排在第一条后面跑,full run 的 linux 墙钟大致翻倍。要调的话
杠杆在 shards_for 上面那段注释里。

## 三处必须跟着改的地方

  * registry / toolstore 缓存键加 matrix.toolchain。两条腿的 runner.os 都是
    Linux,而缓存装的是工具链和编译好的 compat 包 —— 共用一个条目会让 gcc
    的产物替 llvm 回答,正好抹掉这条腿存在的理由。
  * timings artifact 名加 toolchain。upload-artifact@v4 拒绝重名,不加的话两
    条腿会抢 `timings-linux-0`,第二个直接把 job 弄失败。
  * member-timings.tsv 只吃 default 腿。llvm 腿跑的是同一批成员,把每条腿都
    glob 进去会让每个 (platform, member) 出现两行(sort -u 留不住,秒数不
    同),而 shards_for 是把匹配行全加起来的 —— linux 的工作量会读成约两倍,
    shard 数被永久顶到上限。step summary 里则按腿分别列,两条腿是不同的构建,
    平均它们谁也不描述。

## 已知会红

catch2-v2-main 在装了系统 Catch2 v3 的机器上会失败:catch2_main.cpp 用
__has_include(<catch2/catch_all.hpp>) 判 v2/v3,这个探测同样会落到系统目录。
GitHub runner 不装 catch2,所以这条腿上它是绿的(实测 #183 的 CI:linux /
macos / windows 三平台 catch2-v2-main 全 ok)。真要修得等 per-version build
blocks(mcpp#290)。

本地只在 llvm 下取样跑了 19/60 个成员,其余 41 个(grpc / protobuf / godot /
llamacpp / openssl 等重型)受本机磁盘所限没跑 —— 这条腿的第一轮就是它们第一
次在 linux 上被 llvm 编译,可能还有别的既有问题被照出来。刻意不加
continue-on-error:一条非阻塞的腿很容易被永久无视,和一条长期红的腿是同一种病。
Sunrisepeak added a commit that referenced this pull request Aug 8, 2026
`catch2_main.cpp` 用 `__has_include(<catch2/catch_all.hpp>)` 区分两个大版本。
这个探测同样会翻系统 include 目录,而 catch_all.hpp 正是每个发行版的 catch2
包都会装的头。于是在一台装了系统 Catch2 v3 的机器上,一个 v2 消费者被回答
"你是 v3",编出 Catch::Session 那条入口,再死在链接:

    ld.lld: error: undefined symbol: Catch::Session::Session()
    >>> referenced by /usr/include/catch2/catch_session.hpp:39

和 #183 修的 compat.ffmpeg 是同一类问题:宿主机装了同名开发包,vendored 的
东西就被挤掉。gcc 不中招是因为它通过 --sysroot 进 xlings subos,那里没有
/usr/include/catch2 —— 所以这条在 CI 和默认工具链下一直是绿的。

改成探测 `catch2/catch_user_config.hpp.in`:上游把它作为 CMake 模板放在 v3
的源码树里,装的是生成后的 .hpp,模板本身从不安装。三种情形在 clang 22.1.8
和 gcc 16.1.0 上都验过:

    vendored v3 在 -I 上   -> v3     (两种探测一致)
    vendored v2 在 -I 上   -> v2     (catch_all.hpp 答 v3,即本 bug)
    只有系统 v3            -> v2     (catch_all.hpp 答 v3)

四个 catch2 成员 × 两个工具链,在一台装了系统 Catch2 v3 的机器上全过 ——
catch2-v2-main 此前在 llvm 下是 FAIL。

#183 的描述里把这条列为"要等 per-version build blocks(mcpp#290)才能修",
那个判断下早了:要的不是"知道自己是哪个版本",只是一个系统装不出来的探针。
mcpp#290 仍然是更干净的答案,它让这个问题整个消失,而不是换一个探针。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant